﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Gunningine
{
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.Runtime.InteropServices;

    public abstract class RenderSystem : IDisposable
    {
        protected Logger logger;

        protected ResourceManager resourceMgr;

        private Dictionary<string, Texture> textures = new Dictionary<string, Texture>();

        private Dictionary<string, RenderTexture> renderTextures = new Dictionary<string, RenderTexture>();

        private Dictionary<string, Mesh> meshes = new Dictionary<string, Mesh>();

        private Dictionary<string, Shader> shaders = new Dictionary<string, Shader>();

        public int ScreenLeft { get; protected set; }

        public int ScreenWidth { get; protected set; }

        public int ScreenHeight { get; protected set; }

        public int ScreenBottom { get; protected set; }

        public float Fov { get; protected set; }

        /// <summary>
        /// Constructor.
        /// </summary>
        public RenderSystem(ResourceManager resourceMgr)
        {
            this.resourceMgr = resourceMgr;
        }

        /// <summary>
        /// Initialise OpenGL and so on.
        /// </summary>
        /// <param name="x">x min.</param>
        /// <param name="y">y min.</param>
        /// <param name="width">width.</param>
        /// <param name="height">height.</param>
        public abstract void Initialise(int x, int y, int width, int height);

        /// <summary>
        /// Dispose of all resources.
        /// </summary>
        public void DisposeAllResources()
        {
            foreach (var kvp in this.textures)
            {
                kvp.Value.Dispose();
            }
        }

        /// <summary>
        /// Set viewport dimensions.
        /// </summary>
        /// <param name="x">x min.</param>
        /// <param name="y">y min.</param>
        /// <param name="width">width.</param>
        /// <param name="height">height.</param>
        public void SetDimensions(int x, int y, int width, int height)
        {
            this.ScreenLeft = x;
            this.ScreenBottom = y;
            this.ScreenWidth = x + width;
            this.ScreenHeight = y + height;
        }

        /// <summary>
        /// Clear the screen.
        /// </summary>
        public abstract void ClearScreen();

        /// <summary>
        /// Set up viewport and create 3d projection.
        /// </summary>
        /// <param name="fov">horizontal field of view.</param>
        public abstract void Set3dProjection(float fov);

        /// <summary>
        /// Set up viewport and create 2d projection.
        /// </summary>
        public abstract void Set2dProjection();

        /// <summary>
        /// Set the view with the given camera.
        /// </summary>
        /// <param name="camera"></param>
        public abstract void SetCamera(Camera camera);

        /// <summary>
        /// Reset the transform matrix.
        /// </summary>
        public abstract void ResetTransform();

        /// <summary>
        /// Push the current transform.
        /// </summary>
        public abstract void PushTransform();

        /// <summary>
        /// Pop the top of the transform stack.
        /// </summary>
        public abstract void PopTransform();

        /// <summary>
        /// Translate the current transform.
        /// </summary>
        /// <param name="x">x offset.</param>
        /// <param name="y">y offset.</param>
        /// <param name="z">z offset.</param>
        public abstract void Translate(float x, float y, float z);

        /// <summary>
        /// Translate the current tranform.
        /// </summary>
        /// <param name="offset">translation offset.</param>
        public void Translate(Vector3 offset)
        {
            Translate(offset.X, offset.Y, offset.Z);
        }

        /// <summary>
        /// Rotate the current transform.
        /// </summary>
        /// <param name="angle">angle to rotate by.</param>
        /// <param name="x">x-component of rotation axis.</param>
        /// <param name="y">y-component of rotation axis.</param>
        /// <param name="z">z-component of rotation axis.</param>
        public abstract void Rotate(float angle, float x, float y, float z);

        /// <summary>
        /// Rotate the current transform.
        /// </summary>
        /// <param name="angle">angle to rotate by.</param>
        /// <param name="axis">axis to rotate around.</param>
        public void Rotate(float angle, Vector3 axis)
        {
            Rotate(angle, axis.X, axis.Y, axis.Z);
        }

        /// <summary>
        /// Scale the current transform.
        /// </summary>
        /// <param name="x">x amount.</param>
        /// <param name="y">y amount.</param>
        /// <param name="z">z amount.</param>
        public abstract void Scale(float x, float y, float z);

        /// <summary>
        /// Scale the current tranform.
        /// </summary>
        /// <param name="offset">scale amount.</param>
        public void Scale(Vector3 amount)
        {
            Scale(amount.X, amount.Y, amount.Z);
        }

        /// <summary>
        /// Project a 3d coordinate to its location on screen.
        /// </summary>
        /// <param name="position">coordinate.</param>
        /// <param name="camera">camera to use for projection.</param>
        /// <returns></returns>
        public abstract Gunningine.Vector2 Project(Gunningine.Vector3 position, Camera camera);

        /// <summary>
        /// Unproject a screen position to a line in world space.
        /// </summary>
        /// <param name="x">x coord.</param>
        /// <param name="y">y coord</param>
        public abstract Gunningine.Vector3 Unproject(Gunningine.Vector3 position, Camera camera);

        /// <summary>
        /// Return the 6 clipping planes for this projection.
        /// </summary>
        /// <param name="camera"></param>
        /// <returns></returns>
        public abstract Plane[] GetClippingPlanes(Camera camera);

        /// <summary>
        /// Create a new shader.
        /// </summary>
        /// <param name="vertexShader">vertex shader.</param>
        /// <param name="fragmentShader">fragment shader.</param>
        /// <returns></returns>
        protected abstract Shader CreateShaderImpl(TextResource vertexShader, TextResource fragmentShader);

        /// <summary>
        /// Create a new shader to use.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="vertexShader"></param>
        /// <param name="fragmentShader"></param>
        public void CreateShader(string name, string vertexShader, string fragmentShader)
        {
            TextResource vs = (TextResource)this.resourceMgr.GetResource(vertexShader);
            TextResource fs = (TextResource)this.resourceMgr.GetResource(fragmentShader);

            Shader shader = CreateShaderImpl(vs, fs);
            this.shaders[name] = shader;
        }

        /// <summary>
        /// Get named shader.
        /// </summary>
        /// <param name="name">shader.</param>
        /// <returns></returns>
        public Shader GetShader(string name)
        {
            try
            {
                return this.shaders[name];
            }
            catch (Exception)
            {
                throw new ResourceNotFoundException("Could not find shader '" + name + "'.");
            }
        }

        /// <summary>
        /// Create a new texture.
        /// </summary>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="channels"></param>
        /// <param name="data"></param>
        /// <returns></returns>
        public abstract Texture CreateTextureImpl(int width, int height, int channels, IntPtr data);

        /// <summary>
        /// Create a new texture.
        /// </summary>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="channels"></param>
        /// <param name="data"></param>
        /// <returns></returns>
        public Texture CreateTextureImpl(int width, int height, int channels, byte[] data)
        {
            GCHandle pinnedArray = GCHandle.Alloc(data, GCHandleType.Pinned);
            IntPtr ptr = pinnedArray.AddrOfPinnedObject();

            Texture tex = CreateTextureImpl(width, height, channels, ptr);
            pinnedArray.Free();
            return tex;
        }

        /// <summary>
        /// Create texture from raw data.
        /// </summary>
        /// <param name="name">name of texture.</param>
        /// <param name="width">width.</param>
        /// <param name="height">height.</param>
        /// <param name="channels">channels.</param>
        /// <param name="data">data.</param>
        public void CreateTexture(string name, int width, int height, int channels, byte[] data)
        {
            Texture texture = CreateTextureImpl(width, height, channels, data);
            this.textures[name] = texture;
        }

        /// <summary>
        /// Create an OpenGL texture from an image.
        /// </summary>
        /// <param name="image">image resource.</param>
        public void CreateTexture(ImageResource image)
        {
            System.Drawing.Imaging.PixelFormat pixelFormatSys;

            if (image.Channels == 3)
                pixelFormatSys = System.Drawing.Imaging.PixelFormat.Format24bppRgb;
            else
                pixelFormatSys = System.Drawing.Imaging.PixelFormat.Format32bppArgb;

            BitmapData bitmapData = image.Data.LockBits(new Rectangle(0, 0, image.Width, image.Height), 
                                                        ImageLockMode.ReadOnly,
                                                        pixelFormatSys);

            int dataSize = bitmapData.Width * bitmapData.Height * image.Channels;
            byte[] imageData = new byte[dataSize];

            Marshal.Copy(bitmapData.Scan0, imageData, 0, dataSize);
            CreateTexture(image.Name, image.Width, image.Height, image.Channels, imageData);
            image.Data.UnlockBits(bitmapData);
        }

        /// <summary>
        /// Get named texture.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public Texture GetTexture(string name)
        {
            try
            {
                return this.textures[name];
            }
            catch (Exception)
            {
                throw new ResourceNotFoundException("Could not find texture '" + name + "'.");
            }
        }

        /// <summary>
        /// Create new render texture.
        /// </summary>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="channels"></param>
        /// <returns></returns>
        protected abstract RenderTexture CreateRenderTextureImpl(int width, int height, int channels);

        /// <summary>
        /// Create new render texture.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="channels"></param>
        public void CreateRenderTexture(string name, int width, int height, int channels)
        {
            RenderTexture renderTexture = CreateRenderTextureImpl(width, height, channels);
            this.renderTextures[name] = renderTexture;
        }

        /// <summary>
        /// Get named render texture.
        /// </summary>
        /// <param name="name">render texture.</param>
        /// <returns></returns>
        public RenderTexture GetRenderTexture(string name)
        {
            try
            {
                return this.renderTextures[name];
            }
            catch (Exception)
            {
                throw new ResourceNotFoundException("Could not find render texture '" + name + "'.");
            }
        }

        /// <summary>
        /// Create new mesh.
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        protected abstract Mesh CreateMeshImpl(ModelResource model);

        /// <summary>
        /// Create new mesh.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="model"></param>
        public void CreateMesh(string name, ModelResource model)
        {
            Mesh mesh = CreateMeshImpl(model);
            this.meshes[name] = mesh;
        }

        /// <summary>
        /// Get named mesh.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public Mesh GetMesh(string name)
        {
            try
            {
                return this.meshes[name];
            }
            catch (Exception)
            {
                throw new ResourceNotFoundException("Could not find mesh '" + name + "'.");
            }
        }

        /// <summary>
        /// Set colour.
        /// </summary>
        /// <param name="r">red component [0-1].</param>
        /// <param name="g">green component [0-1].</param>
        /// <param name="b">blue component [0-1].</param>
        /// <param name="a">alpha component [0-1].</param>
        public abstract void SetColour(float r, float g, float b, float a = 1.0f);
           
        /// <summary>
        /// Print text at the specified location.
        /// </summary>
        /// <param name="x">x position.</param>
        /// <param name="y">y position, from top ofscreen.</param>
        /// <param name="font">font to use.</param>
        /// <param name="text">text to print.</param>
        public abstract void Print(int x, int y, string font, string text);
         
        /// <summary>
        /// Render a quad.
        /// </summary>
        /// <param name="p0">pos coord 1</param>
        /// <param name="p1">pos coord 2</param>
        /// <param name="p2">pos coord 3</param>
        /// <param name="p3">pos coord 4</param>
        public abstract void RenderQuad3d(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3);

        /// <summary>
        /// Render a textured quad.
        /// </summary>
        /// <param name="p0">pos coord 1</param>
        /// <param name="uv0">uv-coord 1</param>
        /// <param name="p1">pos coord 2</param>
        /// <param name="uv1">uv-coord 2</param>
        /// <param name="p2">pos coord 3</param>
        /// <param name="uv2">uv-coord 3</param>
        /// <param name="p3">pos coord 4</param>
        /// <param name="uv3">uv-coord 4</param>
        public abstract void RenderTexturedQuad3d(Gunningine.Vector3 p0, Gunningine.Vector2 uv0,
                                                  Gunningine.Vector3 p1, Gunningine.Vector2 uv1,
                                                  Gunningine.Vector3 p2, Gunningine.Vector2 uv2,
                                                  Gunningine.Vector3 p3, Gunningine.Vector2 uv3);
            
        /// <summary>
        /// Render a quad.
        /// </summary>
        /// <param name="p0">pos coord 1</param>
        /// <param name="p1">pos coord 2</param>
        /// <param name="p2">pos coord 3</param>
        /// <param name="p3">pos coord 4</param>
        public abstract void RenderQuad2d(Gunningine.Vector2 p0, Gunningine.Vector2 p1, 
                                          Gunningine.Vector2 p2, Gunningine.Vector2 p3);
            
        /// <summary>
        /// Render a set of lines, specified as (x0,y0,x1,y1) groups.
        /// </summary>
        /// <param name="points"></param>
        public abstract void RenderLineList2d(List<float> points);
            
        /// <summary>
        /// Render a textured quad.
        /// </summary>
        /// <param name="p0">pos coord 1</param>
        /// <param name="uv0">uv-coord 1</param>
        /// <param name="p1">pos coord 2</param>
        /// <param name="uv1">uv-coord 2</param>
        /// <param name="p2">pos coord 3</param>
        /// <param name="uv2">uv-coord 3</param>
        /// <param name="p3">pos coord 4</param>
        /// <param name="uv3">uv-coord 4</param>
        public abstract void RenderTexturedQuad2d(Gunningine.Vector2 p0, Gunningine.Vector2 uv0,
                                                  Gunningine.Vector2 p1, Gunningine.Vector2 uv1,
                                                  Gunningine.Vector2 p2, Gunningine.Vector2 uv2,
                                                  Gunningine.Vector2 p3, Gunningine.Vector2 uv3);
            
        /// <summary>
        /// Render a cube of half-width 'size'.
        /// </summary>
        /// <param name="size">half-width of cube.</param>
        public abstract void RenderPlaceHolderEntity(float size, float r, float g, float b);

        #region IDisposable implementation

        bool _disposed = false;

        public void Dispose()
        {
            if (!this._disposed)
            {
                this._disposed = true;
            }
        }

        #endregion

    }
}
